﻿using System;
using System.Collections.Generic;
using System.Linq;
using Gumini.JulaDB.Data;
using Gumini.JulaDB.Engine;
using Gumini.JulaDB.Enums;
using Gumini.JulaDB.Expressions;
using Gumini.JulaDB.Interfaces;
using Gumini.JulaDB.Provider;

namespace Gumini.JulaDB.Planner
{
    /// <summary>
    /// Represents selecting columns by name in the SELECT statement.
    /// </summary>
    internal class ProjectionNode : UnaryPlanNode
    {
        protected readonly IList<ColumnExpressionItem> _columnItems;
        protected readonly TableHeading<TemporaryColumn> _resultHeading;
        private readonly ColumnNameGenerator _generator = new ColumnNameGenerator();
        private readonly IList<Expression> _groupByExpressions;
        private readonly bool _emptyGroupsAreEqual; // true if there is no GROUP BY but aggregate functions are used

        /// <summary>
        /// Initializes a new instance of the <see cref="ProjectionNode"/> class.
        /// </summary>
        /// <param name="child">The child.</param>
        /// <param name="columnItems">The column names.</param>
        public ProjectionNode(PlanNode child, IList<ColumnExpressionItem> columnItems, IList<Expression> groupByExpressions)
            : base(child)
        {
            var columns = new List<TemporaryColumn>();
            var context = child.Heading.GetTypeContext();
            bool groupByExists = groupByExpressions.Any();
            _groupByExpressions = groupByExpressions;

            var areAggregates = new List<bool>();
            
            foreach (var item in columnItems)
            {
                string columnName = _generator.GetColumnName(item);
                var columnType = item.Expression.InferType(context);
                bool isAggregate = item.Expression.IsAggregate;
                bool isGrouped = groupByExpressions.Contains(item.Expression);

                columns.Add(new TemporaryColumn(columnType, columnName, new List<string>(), item.Expression));
                areAggregates.Add(isAggregate);

                // if there are any column names specified in the GROUP BY clause
                if (groupByExists)
                {
                    // column is nor an aggregate function call neither present in the GROUP BY clause
                    if (!isAggregate && !isGrouped)
                        throw new JulaDbException(ErrorCode.AggregateMismatch, "Column should be either an aggregate expression or be present in the GROUP BY clause.");
                }
            }
            if (groupByExists)
            {
                _emptyGroupsAreEqual = false;
            }
            // there are no GROUP BY columns
            // we still allow aggregate expressions that work on all rows
            else
            {
                // all column expresions are aggregates
                if (areAggregates.All(b => b))
                    _emptyGroupsAreEqual = true;
                // no column expressions are aggregates
                else if (areAggregates.All(b => !b))
                    _emptyGroupsAreEqual = false;
                else
                    throw new JulaDbException(ErrorCode.AggregateMismatch, "When the GROUP BY clause is not specified, all selected columns should be either aggregate or scalar expressions.");
            }

            _columnItems = columnItems;
            _resultHeading = new TableHeading<TemporaryColumn>(columns);
        }

        public override List<Row> Execute()
        {
            // grouping the rows
            var groupMapping = new RowMapping(_emptyGroupsAreEqual);
            foreach (var row in Child.Rows)
            {
                // creating the group key
                var keyValues = new List<TypedValue>();
                foreach (var expression in _groupByExpressions)
                {
                    var context = row.GetEvaluationContext(Child.Heading);
                    keyValues.Add(expression.Evaluate(context));
                }

                // adding the group to dictionary
                groupMapping.Add(keyValues, row);
            }

            // special case when there are no source rows, but there are aggregate functions without GROUP BY
            if (groupMapping.IsEmpty && _emptyGroupsAreEqual)
                groupMapping.Add(new List<TypedValue>());

            // evaluate each group creating a single row for each group
            var result = new List<Row>();
            foreach (var pair in groupMapping)
            {
                var newRow = new Row(_resultHeading.NumberOfColumns);
                int i = 0;
                foreach (var item in _columnItems)
                {
                    int expressionIndex = _groupByExpressions.IndexOf(item.Expression);
                    object columnValue;

                    // expresion was not in the GROUP BY statement
                    // we have to evaluate the group of rows with an aggregate function to get a single value
                    if (expressionIndex == -1)
                    {
                        columnValue = item.Evaluate(pair.Value, Child.Heading);
                    }
                    // expression was in the GROUP BY statement
                    // we get the value from the group key
                    else
                    {
                        columnValue = pair.Key[expressionIndex].Value;
                    }
                    newRow.Assign(i, _resultHeading[i].Type, columnValue);
                    i++;
                }
                result.Add(newRow);
            }

            return result;
        }

        public override IHeading<TemporaryColumn> GetColumns()
        {
            return _resultHeading;
        }

        protected override string GetNodeDescription()
        {
            return String.Format("PROJECTION ({0})",
                String.Join(", ", _columnItems.Select(i => String.Format("{0} [{1}]", i.Expression, i.Alias))));
        }
    }
}
